package yawning.mew.bear;

import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import yawning.mew.BuffsDebuffs.Buff;
import yawning.mew.Formulation;
import yawning.mew.MewProperty;
import yawning.mew.MewView;
import yawning.mew.ProcEffect;
import yawning.mew.Procs;
import yawning.mew.PropertiesBase;
import yawning.mew.Report;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.Report.Stat;
import yawning.mew.character.CharacterModel;
import yawning.mew.character.Toon;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.target.Target;
import yawning.mew.target.Target.TargetProperty;

public class BearMitigationFormulation implements Formulation {
	public static class FormulationProperty {
		public static final MewProperty REPORTTTL = new MewProperty("BearMitigationFormulation.ReportTTL", false);	// Report TTL instead of mitigation.
		public static final MewProperty INCOMINGHPS = new MewProperty("BearMitigationFormulation.IncomingHPS", 0.0);	// Incoming HPS for TTL.
		public static final MewProperty PULVERIZEUPTIME = new MewProperty("BearMitigationFormulation.PulverizeUptime", 0.8);	// Pulverize uptime.
	}

	private class BearMitigationFormulationReport extends Report {
		String mString;

		BearMitigationFormulationReport(String s) {
			mString = s;
		}

		@Override
		public String toString() {
			return mString;
		}
	}

	private MewView mView;
	private BearModel mModel;
	private HashMap<ProcEffect, Double> mProcUptimes;
	private PropertiesBase mProperties;

	private int mEncounterDuration;

	private class CycleResults {
		double mNrWhiteAttacks;
		double mNrYellowAttacks;
		double mNrDotTicks;
		double mCycleTime;
	}

	@Override
	public Report calculate(Toon toon, Target target, PropertiesBase config, MewView view) {
		mView = view;
		mModel = new BearModel(toon, config);
		mModel.setTarget(target);
		mProcUptimes = new HashMap<ProcEffect, Double>();
		mProperties = config;

		// Supplied parameters.
		// TODO: Add support for the mob's yellow swing.
		mEncounterDuration = config.getInt(Property.SIMULATION_DURATION);
		double bossAttackSpeed = target.getConfig().getDouble(TargetProperty.TARGET_SWING_INTERVAL);
		bossAttackSpeed *= mModel.multInfectedWounds;
		double bossDamage = target.getConfig().getDouble(TargetProperty.TARGET_SWING_DAMAGE);
		double healsPerSec = config.getDouble(FormulationProperty.INCOMINGHPS);
		boolean reportTTL = config.getBool(FormulationProperty.REPORTTTL);

		// Formulate uptime based raid buffs.
		if (mModel.mBuffsDebuffs.isBuff(Buff.MAJORHASTE)) {
			double heroismUpDuration = mModel.GetCooldownUptime(mModel.heroismBuffDuration, mModel.heroismCDDuration, mEncounterDuration);
			double heroismUptime = (double) heroismUpDuration / mEncounterDuration;
			mModel.multiplyHaste(mModel.buffHeroism * heroismUptime + (1 - heroismUptime));
		}

		if (mModel.mBuffsDebuffs.isBuff(Buff.UNHOLYFRENZY)) {
			double unholyFrenzyUpDuration = mModel.GetCooldownUptime(mModel.unholyFrenzyBuffDuration, mModel.unholyFrenzyCDDuration, mEncounterDuration);
			double unholyFrenzyUptime = (double) unholyFrenzyUpDuration / mEncounterDuration;
			mModel.multiplyHaste(mModel.buffUnholyFrenzy * unholyFrenzyUptime + (1 - unholyFrenzyUptime));
		}

//		double targetArmorMinorMult = 1.0;
//		if (mModel.mBuffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) {
//			double shatteringThrowUpDuration = mModel.GetCooldownUptime(mModel.shatteringThrowDebuffDuration, mModel.shatteringThrowCDDuration, mEncounterDuration);
//			double shatteringThrowUptime = (double) shatteringThrowUpDuration / mEncounterDuration;
//			targetArmorMinorMult = mModel.debuffShatteringThrow * shatteringThrowUptime + (1 - shatteringThrowUptime);
//		}

		// TODO: TOTT, only relevant for threat

		// Outgoing yellow attacks.
		//
		// TODO:
		//  * Derive this instead of hardcoding.
		//  * Consider supporting thrash, also this assumes 100% Lacerate uptime which is extremely over-optimistic (~25% too high).
		//  * We currently are not factoring in yellow attacks lost to having to maintain Demo.

		double dotInterval = mModel.lacerateTickInterval;
		double maulInterval = 3.0;
		double yellowInterval = 1.5;
		double pulverizeUptime = Math.max(0, Math.min(config.getDouble(FormulationProperty.PULVERIZEUPTIME), 1));
		pulverizeUptime = mModel.mToon.getTalent(BearModel.PULVERIZE) > 0 ? pulverizeUptime : 0;
		double manglesToYellows = 0.35;
		double pPulverizeCrit = mModel.mToon.getTalent(BearModel.PULVERIZE) > 0 ? 0.09 * pulverizeUptime : 0;
		mModel.addMeleeCrit(pPulverizeCrit);

		double attackSpeed = mModel.getMeleeInterval();
		double pHitChance = 1 - (mModel.getMissedChance() + mModel.getDodgedChance() + mModel.getParriedChance());

		CycleResults outgoingAttacks = new CycleResults();
		outgoingAttacks.mCycleTime = 30;
		outgoingAttacks.mNrWhiteAttacks = outgoingAttacks.mCycleTime / mModel.getMeleeInterval() * pHitChance;
		outgoingAttacks.mNrDotTicks = outgoingAttacks.mCycleTime / dotInterval;
		outgoingAttacks.mNrYellowAttacks = outgoingAttacks.mCycleTime / yellowInterval * pHitChance;

		// Apply trinkets/Potion
		calculateProcEffects(outgoingAttacks);

		// TODO: Recalculate outgoing yellow attacks.

		// Formulate Savage Defense
		attackSpeed = mModel.getMeleeInterval();
		pHitChance = 1 - (mModel.getMissedChance() + mModel.getDodgedChance() + mModel.getParriedChance());
		double SY = bossAttackSpeed * ( 1 / maulInterval + 1 / yellowInterval);
		double SW = bossAttackSpeed / attackSpeed;
		double CY = Math.min(1.0, pHitChance * mModel.getMeleeCritChance());
		double CW = mModel.getWhiteCritChance();
		double m;
		if (mModel.mToon.tier_13_2pc) {
			// If you're going to guestimate like this, at least factor Pulverize uptime into your estimation. :(
			double pulvM = Math.pow(1 - (CY * (mModel.mToon.tier_13_2pc ? (1 + manglesToYellows) : 1) / 2), SY) * Math.pow(1 - (CW / 2), SW);
			double nonPulvM = Math.pow(1 - CY * 1 / 2, SY) * Math.pow(1 - (CW / 2), SW);
			m = pulvM * pulverizeUptime + nonPulvM * (1- pulverizeUptime);
		} else
			m = Math.pow(1 - CY * 1 / 2, SY) * Math.pow(1 - (CW / 2), SW);
		double pBossFail = Math.min(1.0, mModel.getDodgeChance() + mModel.getMissChance());
		double SDUptime = (1 - m) / (1 - m * pBossFail);

		// Formulate mitigation
		double armorDmgReduction = CharacterModel.calculateArmorDR(mModel.getArmor(), mModel.mToon.level + 3);
		mModel.multiplyIncPhysicalDamage(mModel.multDemoralizingRoarDR);	// Apply Demoralizing Roar
		double mitigatedBossDmg = bossDamage * mModel.getIncPhysicalDamageMult() * (1 - armorDmgReduction);
		double vengeanceCap = mModel.getVengeanceCap();
		double meanBossAttackHit = 1 / (1 - pBossFail);
		double avgVengeanceDecay;
		if (mModel.bug_vengeanceDecay)
				avgVengeanceDecay = Math.max(0, 0.1 * (bossAttackSpeed - 2) / 2);
		else
				avgVengeanceDecay = 1 - Math.pow(0.95, (meanBossAttackHit - 1) * bossAttackSpeed / 2) + Math.max(0, 0.1 * (bossAttackSpeed - 2) / 2);
		double avgVengeanceAttackPower = vengeanceCap * (1 - avgVengeanceDecay);
		mModel.addAttackPower(avgVengeanceAttackPower);
		double avgSDAbsorb = Math.min(mModel.getAttackPower() * 0.35 * mModel.getSavageDefenseMastery(), mitigatedBossDmg);
		double SDMitigation = SDUptime * avgSDAbsorb / mitigatedBossDmg;
		double totalMitigation = 1 - (1 - SDMitigation) * (1 - pBossFail) * mitigatedBossDmg / bossDamage;
		double mitigationValue = 1 / (1 - totalMitigation);
		double equivalentHealth = mModel.getHealth() / (1 - totalMitigation);
		double damageTakenPerSec = bossDamage * (1 - totalMitigation);
		double timeToLive = mModel.getHealth() / (damageTakenPerSec - healsPerSec);

		// Generate the report.
		StringBuilder s = new StringBuilder();

		if (!reportTTL) {
			s.append("Mitigation Value: ").append(mitigationValue).append('\n');
		} else {
			s.append("Time to Live: ").append(timeToLive).append('\n');
		}
		s.append('\n');

		s.append("Attack Speed: ").append(attackSpeed).append('\n');
		s.append("Hit Chance: ").append(pHitChance).append('\n');
		s.append("SY: ").append(SY).append('\n');
		s.append("SW: ").append(SW).append('\n');
		s.append("CY: ").append(CY).append('\n');
		s.append("CW: ").append(CW).append('\n');
		s.append("m: ").append(m).append('\n');
		s.append("Boss Fail Chance: ").append(pBossFail).append('\n');
		s.append("SD Uptime: ").append(SDUptime).append('\n');
		s.append("Armor Dmg Reduction: ").append(armorDmgReduction).append('\n');
		s.append("Mitigated Boss Dmg: ").append(mitigatedBossDmg).append('\n');
		s.append("Vengeance Cap: ").append(vengeanceCap).append('\n');
		s.append("Mean Boss Attack / Hit: ").append(meanBossAttackHit).append('\n');
		s.append("Avg Vengeance Decay: ").append((avgVengeanceDecay < 1) ? avgVengeanceDecay : 0).append('\n');
		s.append("Avg Attack Power: ").append(mModel.getAttackPower()).append('\n');
		s.append("Avg SD Absorbs: ").append(avgSDAbsorb).append('\n');
		s.append("SD Mitigation: ").append(SDMitigation).append('\n');
		s.append("Total Mitigation: ").append(totalMitigation).append('\n');
		if (reportTTL) s.append("Mitigation Value: ").append(mitigationValue).append('\n');
		s.append("Equivalent Health: ").append(equivalentHealth).append('\n');
		s.append("Damage Taken / sec: ").append(damageTakenPerSec).append('\n');
		if (!reportTTL) s.append("Time to Live: ").append(timeToLive).append('\n');

		if (mProcUptimes != null && mProcUptimes.isEmpty() == false) {
			s.append('\n');
			for (Map.Entry<ProcEffect, Double> entry : mProcUptimes.entrySet()) {
				ProcEffect proc = entry.getKey();
				Double uptime = entry.getValue();

				if (proc.buffDuration == 0)
					s.append(proc.name).append(" Procs: ").append(Report.round(uptime * mEncounterDuration / outgoingAttacks.mCycleTime)).append('\n');
				else
					s.append(proc.name).append(" Uptime (%): ").append(Report.round(uptime * 100)).append('\n');
			}
		}

		BearMitigationFormulationReport report = new BearMitigationFormulationReport(s.toString());

		report.setResult(Stat.TIME_TO_LIVE, timeToLive);
		report.setResult(Stat.MITIGATION_VALUE, mitigationValue);
		report.setResult(Stat.DAMAGE_DEALT_PER_SECOND, 0);

		return report;
	}

	private void calculateProcEffects(CycleResults results)
	{
		Procs procsInstance = Procs.getInstance();
		List<ProcEffect> procEffects = null;

		procEffects = procsInstance.getProcsByProperty(mProperties.getProperties());

		for (Iterator<ProcEffect> iter = procEffects.iterator(); iter.hasNext(); ) {
			ProcEffect effect = iter.next();

			// Figure out uptime.
			double uptime = 0;
			double procRate = Math.max(effect.rate, effect.ppm * (mModel.getBaseWeaponSpeed() / 60));
			double nrTriggers = 0;
			double buffDuration = (effect.buffDuration > 0) ? effect.buffDuration : 1;

			if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.USER))) {
				   uptime = (double) buffDuration / effect.buffCooldown;
			} else if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
				if (mProperties.getBool(Property.SIMULATION_PREPOTION)) buffDuration *= 2;
				uptime = (double) buffDuration / mEncounterDuration;
			} else {
				for (ProcEffect.Trigger t : effect.triggers) {
					switch (t) {
						case POTION:
							// Should never happen.
							break;
						case USER:
							// Should never happen.
							break;
						case WHITE:
							nrTriggers += results.mNrWhiteAttacks * (1 - mModel.getWhiteCritChance());
							break;
						case WHITECRIT:
							nrTriggers += results.mNrWhiteAttacks * mModel.getWhiteCritChance();
							break;
						case YELLOW:
							nrTriggers += results.mNrYellowAttacks * (1 - mModel.getMeleeCritChance());
							break;
						case YELLOWCRIT:
							nrTriggers += results.mNrYellowAttacks * mModel.getMeleeCritChance();
							break;
						case DOT:
							nrTriggers += results.mNrDotTicks * (1- mModel.getMeleeCritChance());
							break;
						case DOTCRIT:
							nrTriggers += results.mNrDotTicks * mModel.getMeleeCritChance();
							break;
					}
				}

				nrTriggers /= results.mCycleTime;

				uptime = mModel.GetProcUptime(procRate, nrTriggers, buffDuration, effect.buffCooldown);
			}

			// Calculate the effect.
			double procMult = Math.max(effect.buffStackMax, 1);
			if (effect.buffStackMax > 1) {
				if (uptime < 1.0) {
					// Ok, the proc has non-100% uptime, and stacks, I need to take this into account.
					if (effect.get(Effect.SPELL_ID) == 0) {
						//
						// These need to be special cased.  I doubt anyone still uses a BNS, and
						// Victor's Call is garbage so just print out a warning for now.
						//
						mView.onWarn("Proc effect \"" + effect.name + "\" not supported. (Stacks and has non-100% uptime)");
						continue;
					}
				} else {
					// Only multiplier that affects this is uptime, since we do not care about ramp duration.
					procMult *= uptime;
				}
			} else {
				// Everything else just gets a multiplier that is uptime based.
				procMult *= uptime;
			}

			if (effect.get(Effect.SPELL_ID) != 0) {
				// Proc effects that require special handling go here.
				if (effect.get(Effect.SPELL_ID) == 69150 || effect.get(Effect.SPELL_ID) == 68994) {
					// Matrix Restabilizer (391/378)

					// XXX: Doing this introduces a ordering dependency on other procs.
//                	double critRating = getCritRating();
//                	double hasteRating = getHasteRating();
//                	double masteryRating = getMasteryRating();

					double critRating = mModel.mToon.critRating;
					double hasteRating = mModel.mToon.hasteRating;
					double masteryRating = mModel.mToon.masteryRating;

					if (masteryRating >= critRating && masteryRating >= hasteRating) { mModel.addMasteryRating(effect.get(Effect.MASTERY_RATING) * procMult); }
					else if (hasteRating >= critRating && hasteRating >= masteryRating) { mModel.addHasteRating(effect.get(Effect.HASTE_RATING) * procMult); }
					else if (critRating >= hasteRating && critRating >= masteryRating) { mModel.addCritRating(effect.get(Effect.CRIT_RATING) * procMult); }
				} else if (effect.get(Effect.SPELL_ID) == 26297) {
					// Troll Berserking
					mModel.multiplyMeleeHaste(1.2 * procMult + (1 - procMult));
				} else if (effect.get(Effect.SPELL_ID) == 62051) {
					// DMC:Hurricane - As of 4.2 PTR, this is incapable of critting.
					// Note: Do not care about static damage at all.
					//procDamageSpell += 7000 * procMult;
				} else if (effect.get(Effect.SPELL_ID) == 77999 || effect.get(Effect.SPELL_ID) == 77207 || effect.get(Effect.SPELL_ID) == 77979) {
					// Vial of Shadows - Damage only, no need to model.
				} else if (effect.get(Effect.SPELL_ID) == 78473 || effect.get(Effect.SPELL_ID) == 77194 || effect.get(Effect.SPELL_ID) == 78482) {
					// Kiril, Fury of the Beasts
					// Note: I assume you have 1 sec of 0 +Agi during ramp up.
					double totalAgi = (effect.get(Effect.AGILITY) * 10 * 10 + effect.get(Effect.AGILITY) * 45) / buffDuration;
					mModel.addAgility(totalAgi);
				} else {
					mView.onWarn("Proc effect \"" + effect.name + "\" not supported.  (Requires special handling.)");
					continue;
				}
			} else {
				if (effect.get(Effect.STRENGTH) > 0) {
					mModel.addStrength(effect.get(Effect.STRENGTH) * procMult);
				}
				if (effect.get(Effect.AGILITY) > 0) {
					mModel.addAgility(effect.get(Effect.AGILITY) * procMult);
				}
				if (effect.get(Effect.ATTACK_POWER) > 0) {
					mModel.addAttackPower(effect.get(Effect.ATTACK_POWER) * procMult);
				}
				if (effect.get(Effect.HASTE_RATING) > 0) {
					mModel.addHasteRating(effect.get(Effect.HASTE_RATING) * procMult);
				}
				if (effect.get(Effect.CRIT_RATING) > 0) {
					mModel.addCritRating(effect.get(Effect.CRIT_RATING) * procMult);
				}
				if (effect.get(Effect.MASTERY_RATING) > 0) {
					mModel.addMasteryRating(effect.get(Effect.MASTERY_RATING) * procMult);
				}
				if (effect.get(Effect.WEAPON_DAMAGE) > 0) {
					mModel.addWeaponDamage(effect.get(Effect.WEAPON_DAMAGE) * procMult);
				}
				if (effect.get(Effect.DODGE_RATING) > 0) {
					mModel.addDodgeRating(effect.get(Effect.DODGE_RATING) * procMult);
				}

				// TODO: Procing hit/expertise rating is somewhat nonsensical, I should handle this better.
				if (effect.get(Effect.HIT_RATING) > 0) {
					mModel.addHitRating(effect.get(Effect.HIT_RATING) * procMult);
				}
				if (effect.get(Effect.EXPERTISE_RATING) > 0) {
					mModel.addExpertiseRating(effect.get(Effect.EXPERTISE_RATING) * procMult);
				}

				// Note: Do not care about static damage at all.
//				if (effect.get(Effect.PHYSICAL_DAMAGE) > 0) {
//					double dmgMult = (1 - pFail) * ((1 - getMeleeCritChance()) + getMeleeCritChance() * multCrit);
//					procDamagePhys += effect.get(Effect.PHYSICAL_DAMAGE) * procMult * dmgMult;
//				}
//				if (effect.get(Effect.SPELL_DAMAGE) > 0) {
//					double dmgMult = (1 - getSpellMissChance()) * ((1 - getSpellCritChance()) + getSpellCritChance() * multSpellCrit);
//					procDamageSpell += effect.get(Effect.SPELL_DAMAGE) * procMult * dmgMult;
//				}
			}

			// Store the uptime statistics.
			if (effect.buffDuration == 0)
				mProcUptimes.put(effect, uptime * results.mCycleTime);
			else
				mProcUptimes.put(effect, uptime);
		}
	}
}
